/*******************************************************************************
 * Copyright (c) 2011, 2014 IBM Corporation.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * and Eclipse Distribution License v. 1.0 which accompanies this distribution.
 *
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 *
 *    Steve Speicher - initial API and implementation
 *    Samuel Padgett - add support for creating and updating resource using shapes
 *******************************************************************************/
package org.eclipse.lyo.testsuite.oslcv2.core;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathException;
import org.apache.http.HttpResponse;
import org.apache.http.util.EntityUtils;
import org.apache.wink.json4j.JSON;
import org.apache.wink.json4j.JSONArray;
import org.apache.wink.json4j.JSONException;
import org.apache.wink.json4j.JSONObject;
import org.eclipse.lyo.testsuite.util.OSLCConstants;
import org.eclipse.lyo.testsuite.util.OSLCUtils;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.xml.sax.SAXException;

/**
 * This class provides JUnit tests for the validation of the OSLCv2 creation and
 * updating of change requests. It uses the template files specified in
 * setup.properties as the entity to be POST or PUT, for creation and updating
 * respectively. If these files are not defined, it uses the resource shapes for
 * the creation factory.
 *
 * After each test, it attempts to perform a DELETE call on the resource that
 * was presumably created, but this DELETE call is not technically required in
 * the OSLC spec, so the created change request may still exist for some service
 * providers.
 */
@RunWith(Parameterized.class)
public class CreationAndUpdateJsonTests extends CreationAndUpdateBaseTests {

    private HashMap<String, String> namespacePrefixMap = new HashMap<String, String>();
    private static final HashMap<String, String> KNOWN_PREFIXES = new HashMap<String, String>();

    static {
        KNOWN_PREFIXES.put(OSLCConstants.DC, "dcterms");
        KNOWN_PREFIXES.put(OSLCConstants.RDF, "rdf");
        KNOWN_PREFIXES.put(OSLCConstants.RDFS, "rdfs");
        KNOWN_PREFIXES.put(OSLCConstants.OSLC_V2, "oslc");
        KNOWN_PREFIXES.put(OSLCConstants.OSLC_CM_V2, "oslc_cm");
    }

    public CreationAndUpdateJsonTests(String url) {
        super(url);
    }

    @Parameters
    public static Collection<Object[]> getAllDescriptionUrls()
            throws IOException,
                    ParserConfigurationException,
                    SAXException,
                    XPathException,
                    JSONException {
        ArrayList<String> serviceUrls = getServiceProviderURLsUsingJson(null);

        // TODO: We should use JSON for this.
        return toCollection(
                getCapabilityURLsUsingRdfXml(
                        OSLCConstants.CREATION_PROP,
                        serviceUrls,
                        useDefaultUsageForCreation,
                        null));
    }

    @Override
    public String getContentType() {
        return OSLCConstants.CT_JSON;
    }

    @Override
    public String getCreateContent() throws IOException, JSONException {
        if (jsonCreateTemplate == null) {
            String shapeUri = getShapeUriForCreation(currentUrl);
            return createResourceFromShape(shapeUri);
        }

        return jsonCreateTemplate;
    }

    @Override
    public String getUpdateContent(String resourceUri) throws JSONException, IOException {
        if (jsonUpdateTemplate == null) {
            return updateResourceFromShape(resourceUri);
        }

        return jsonUpdateTemplate;
    }

    private String createResourceFromShape(String shapeUri) throws IOException, JSONException {
        JSONObject toCreate = new JSONObject();
        JSONObject shape = getResource(shapeUri);

        fillType(toCreate, shape);
        fillRequiredProperties(toCreate, shape);
        fillPrefixes(toCreate);

        return toCreate.write();
    }

    /*
     * Fill in the first type from the oslc:describes property in the shape.
     */
    private void fillType(JSONObject toCreate, JSONObject shape) throws JSONException {
        if (shape.has("oslc:describes")) {
            JSONArray describes = shape.getJSONArray("oslc:describes");
            if (!describes.isEmpty()) {
                JSONObject firstType = describes.getJSONObject(0);
                toCreate.put("rdf:type", firstType);
            }
        }
    }

    /*
     * Add reasonable values for any required properties.
     */
    private void fillRequiredProperties(JSONObject toCreate, JSONObject shape)
            throws JSONException, IOException {
        // Look at each property.
        JSONArray array = shape.getJSONArray("oslc:property");
        for (int i = 0; i < array.length(); ++i) {
            JSONObject property = array.getJSONObject(i);

            // Only try to fill in required properties to minimize the chance of errors.
            if (isPropertyRequired(property)) {
                fillInProperty(toCreate, property);
            }
        }
    }

    private void fillInProperty(JSONObject toCreate, JSONObject property)
            throws JSONException, IOException {
        // Try to use an allowed value.
        JSONArray allowedValueArray = getAllowedValues(property);
        if (allowedValueArray != null && !allowedValueArray.isEmpty()) {
            int index = (allowedValueArray.length() > 1) ? 1 : 0;
            toCreate.put(getQName(property), allowedValueArray.get(index));
        } else {
            // No allowed values. Do our best to fill in a value from the value type.
            fillInUsingValueType(toCreate, property);
        }
    }

    /*
     * Tries to fill in a reasonable property value based on the value type.
     */
    private void fillInUsingValueType(JSONObject toCreate, JSONObject property)
            throws JSONException {
        Object valueTypeObject = property.get("oslc:valueType");
        String key = getQName(property);
        if (valueTypeObject != null) {
            HashSet<String> valueTypes = getValueTypes(property);

            /*
             * Look at each type. Try to fill in something reasonable.
             */
            if (valueTypes.contains(OSLCConstants.STRING_TYPE)
                    || valueTypes.contains(OSLCConstants.XML_LITERAL_TYPE)) {
                String string = generateStringValue(getMaxSize(property));
                toCreate.put(key, string);
            } else if (valueTypes.contains(OSLCConstants.BOOLEAN_TYPE)) {
                toCreate.put(key, true);
            } else if (valueTypes.contains(OSLCConstants.INTEGER_TYPE)) {
                toCreate.put(key, 1);
            } else if (valueTypes.contains(OSLCConstants.DOUBLE_TYPE)) {
                toCreate.put(key, 1.0d);
            } else if (valueTypes.contains(OSLCConstants.FLOAT_TYPE)) {
                toCreate.put(key, 1.0f);
            }

            // TODO: Support decimal, date/time, and resource.

        } else {
            // We have no hints. Try to set a string value. This may fail.
            String string = generateStringValue(getMaxSize(property));
            toCreate.put(key, string);
        }
    }

    private String getQName(JSONObject property) throws JSONException {
        String propertyDefinition =
                property.getJSONObject("oslc:propertyDefinition").getString("rdf:resource");
        String name = property.getString("oslc:name");
        String namespace =
                propertyDefinition.substring(0, propertyDefinition.length() - name.length());
        String prefix = getPrefix(namespace);
        assertNotNull("No prefix for namespace: " + namespace, prefix);

        return prefix + ":" + name;
    }

    private JSONArray getAllowedValues(JSONObject property) throws JSONException, IOException {
        if (property.has("oslc:allowedValue")) {
            return property.getJSONArray("oslc:allowedValue");
        }

        if (property.has("oslc:allowedValues")) {
            JSONObject allowedValuesObj = property.getJSONObject("oslc:allowedValues");
            // Strangely, RTC inlines oslc:allowedValues.
            if (allowedValuesObj.has("oslc:allowedValue")) {
                return allowedValuesObj.getJSONArray("oslc:allowedValue");
            }

            if (allowedValuesObj.has("rdf:resource")) {
                String allowedValuesUri = allowedValuesObj.getString("rdf:resource");
                JSONObject allowedValues = getResource(allowedValuesUri);
                return allowedValues.getJSONArray("oslc:allowedValue");
            }
        }

        return null;
    }

    /*
     * Determine the value types for this property, a hash set of URIs (as strings).
     */
    private HashSet<String> getValueTypes(JSONObject property) throws JSONException {
        HashSet<String> valueTypes = new HashSet<String>();

        // Some providers represent value type as an array, some as an object.
        Object valueTypeValue = property.get("oslc:valueType");
        if (valueTypeValue instanceof JSONObject o) {
            valueTypes.add(o.getString("rdf:resource"));
        } else if (valueTypeValue instanceof JSONArray a) {
            Iterator<?> i = a.iterator();
            while (i.hasNext()) {
                JSONObject next = (JSONObject) i.next();
                valueTypes.add(next.getString("rdf:resource"));
            }
        } else {
            fail("Incorrect type for oslc:valueType for property " + property.getString("name"));
        }

        return valueTypes;
    }

    private boolean isPropertyRequired(JSONObject property) throws JSONException {
        if (!property.has("oslc:occurs")) {
            return false;
        }

        JSONObject occurs = property.getJSONObject("oslc:occurs");
        String occursValue = occurs.getString("rdf:resource");

        return isPropertyRequired(occursValue);
    }

    private Integer getMaxSize(JSONObject property) throws JSONException {
        if (property.has("oslc:maxSize")) {
            return property.getInt("oslc:maxSize");
        }

        return null;
    }

    private boolean isPropertyReadOnly(JSONObject property) throws JSONException {
        if (!property.has("oslc:readOnly")) {
            return false;
        }

        return property.getBoolean("oslc:readOnly");
    }

    private String getPrefix(String namespace) {
        String prefix = namespacePrefixMap.get(namespace);
        if (prefix == null) {
            prefix = KNOWN_PREFIXES.get(namespace);
            if (prefix == null) {
                prefix = "j" + (namespacePrefixMap.size() + 1);
            }
            namespacePrefixMap.put(namespace, prefix);
        }

        return prefix;
    }

    private void fillPrefixes(JSONObject resource) throws JSONException {
        namespacePrefixMap.put(OSLCConstants.RDF, "rdf");
        JSONObject prefixes = new JSONObject();
        for (Map.Entry<String, String> next : namespacePrefixMap.entrySet()) {
            prefixes.put(next.getValue(), next.getKey());
        }

        resource.put("prefixes", prefixes);
    }

    private String updateResourceFromShape(String uri) throws JSONException, IOException {
        JSONObject toUpdate = getResource(uri);
        JSONObject instanceShapeResource = getInstanceShape(toUpdate);
        modifySomeProperty(toUpdate, instanceShapeResource);

        return toUpdate.write();
    }

    private JSONObject getResource(String uri) throws IOException, JSONException {
        HttpResponse resp =
                OSLCUtils.getResponseFromUrl(uri, null, creds, OSLCConstants.CT_JSON, headers);
        try {
            assertEquals(
                    "Failed to get resource at " + uri, 200, resp.getStatusLine().getStatusCode());
            return (JSONObject) JSON.parse(resp.getEntity().getContent());
        } finally {
            EntityUtils.consume(resp.getEntity());
        }
    }

    private JSONObject getInstanceShape(JSONObject toUpdate) throws JSONException, IOException {
        assertTrue(
                "No instance shape for resource: " + toUpdate.write(),
                toUpdate.has("oslc:instanceShape"));
        String instanceShapeUri =
                toUpdate.getJSONObject("oslc:instanceShape").getString("rdf:resource");
        JSONObject instanceShapeResource = getResource(instanceShapeUri);

        return instanceShapeResource;
    }

    private void modifySomeProperty(JSONObject toUpdate, JSONObject instanceShapeResource)
            throws JSONException {
        JSONArray array = instanceShapeResource.getJSONArray("oslc:property");
        for (int i = 0; i < array.length(); ++i) {
            JSONObject property = array.getJSONObject(i);

            if (isPropertyReadOnly(property)) {
                continue;
            }

            // Like AbstractCreationAndUpdateRdfTests, let's try to keep things
            // simple and find a string property to modify. This test will fail
            // if there isn't an editable string property for the resource.
            HashSet<String> valueTypes = getValueTypes(property);

            if (valueTypes.contains(OSLCConstants.STRING_TYPE)
                    || valueTypes.contains(OSLCConstants.XML_LITERAL_TYPE)) {
                String string = generateStringValue(getMaxSize(property));
                toUpdate.put(getQName(property), string);
                return;
            }
        }
    }
}
